package com.xy.job.admin.service

import com.xy.job.admin.api.request.*
import com.xy.job.admin.common.STANDARD_DATE_FORMAT
import com.xy.job.admin.core.schedule.Scheduler
import com.xy.job.admin.core.schedule.TriggerTask
import com.xy.job.admin.domain.JobInfo
import com.xy.job.admin.enums.NodeType
import com.xy.job.admin.enums.ScheduleType
import com.xy.job.admin.enums.ScheduleType.*
import com.xy.job.admin.repository.JobInfoRepository
import com.xy.job.admin.repository.JobInstanceRepository
import com.xy.job.admin.repository.NodeRegistryRepository
import com.xy.job.core.enums.JobStatus
import com.xy.job.core.enums.JobType.JAVA
import com.xy.job.core.enums.JobType.SCRIPT
import com.xy.job.core.exception.BizException
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.data.domain.*
import org.springframework.scheduling.support.CronExpression
import org.springframework.stereotype.Service
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException
import java.time.temporal.ChronoUnit

@Service
class JobInfoService(
    var scheduler: Scheduler,
    var nodeRegistryRepository:NodeRegistryRepository,
    var jobInstanceRepository: JobInstanceRepository,
    var jobInfoRepository: JobInfoRepository,
) {

    private val log: Logger = LoggerFactory.getLogger(this::class.java)

    fun get(jobId: Long): JobInfo {
        return jobInfoRepository.findById(jobId).orElseThrow()
    }

    fun query(param: JobInfoQueryParam): Page<JobInfo> {
        val pageable: Pageable = PageRequest.of(
            param.pageNo - 1, param.pageSize, Sort.by(JobInfo::id.name).descending()
        )
        val jobInfo = JobInfo().apply {
            this.groupCode = param.groupCode
            this.jobName = param.jobName
            this.jobHandler = param.jobHandler
        }
        val matcher = ExampleMatcher.matchingAll()
            .withMatcher(JobInfo::groupCode.name, ExampleMatcher.GenericPropertyMatchers.exact())
            .withMatcher(JobInfo::jobName.name, ExampleMatcher.GenericPropertyMatchers.contains())
            .withMatcher(JobInfo::jobHandler.name, ExampleMatcher.GenericPropertyMatchers.contains())
        val example = Example.of(jobInfo, matcher)
        return jobInfoRepository.findAll(example, pageable)
    }

    fun add(param: JobInfoAddParam): JobInfo {
        val jobInfo = JobInfo().apply {
            this.groupCode = param.groupCode
            this.jobName = param.jobName
            this.jobDesc = param.jobDesc
            this.scheduleType = param.scheduleType
            this.scheduleConfig = param.scheduleConfig
            this.enabled = false
            this.jobType = param.jobType
            this.jobHandler = when (param.jobType!!) {
                JAVA -> param.jobHandler
                SCRIPT -> "ScriptJobHandler"
            }
            this.executeMode = param.executeMode
            this.executeParams = param.executeParams
            this.nextTriggerTime = nextTriggerTime(scheduleType = param.scheduleType!!, scheduleConfig = param.scheduleConfig!!)
            this.maxConcurrentNum = param.maxConcurrentNum
            this.extendConfig = param.extendConfig
            this.scriptType = param.scriptType
            this.scriptCode = param.scriptCode
            this.lockName = "JOB_LOCK_1"
        }
        jobInfoRepository.save(jobInfo)
        return jobInfo
    }

    fun edit(param: JobInfoEditParam): JobInfo {
        val jobInfo = findByIdOrThrow(param.jobId!!)
        jobInfo.apply {
            this.jobName = param.jobName
            this.jobDesc = param.jobDesc
            this.scheduleType = param.scheduleType
            this.scheduleConfig = param.scheduleConfig
            this.enabled = false
            this.jobType = param.jobType
            this.jobHandler = when (param.jobType!!) {
                JAVA -> param.jobHandler
                SCRIPT -> "ScriptJobHandler"
            }
            this.executeMode = param.executeMode
            this.executeParams = param.executeParams
            this.nextTriggerTime = nextTriggerTime(scheduleType = param.scheduleType!!, scheduleConfig = param.scheduleConfig!!)
            this.maxConcurrentNum = param.maxConcurrentNum
            this.extendConfig = param.extendConfig
            this.scriptType = param.scriptType
            this.scriptCode = param.scriptCode
        }
        jobInfoRepository.save(jobInfo)
        return jobInfo
    }

    fun remove(id: Long): Boolean {
        jobInfoRepository.findById(id).orElseThrow {
            throw BizException("job is not exist, id=[$id]")
        }
        jobInfoRepository.deleteById(id)
        return true
    }

    fun triggerOnce(param: JobTriggerManuallyParam): Long {
        val jobInfo = findByIdOrThrow(param.jobId!!)
        nodeRegistryRepository.findByNodeNameAndNodeType(
            nodeName = jobInfo.groupCode!!,
            nodeType = NodeType.EXECUTOR,
        ).ifEmpty { throw BizException("无可用机器") }

        val existProcessingInstanceCount = jobInstanceRepository.countByJobIdAndJobStatus(
            jobId = jobInfo.id!!,
            jobStatus = JobStatus.PROCESSING,
        )
        if (existProcessingInstanceCount >= jobInfo.maxConcurrentNum!!) {
            throw BizException("当前任务实例已达到最大并发数，无法继续创建新的任务实例")
        }
        val jobInstance = jobInfo.createInstance()
        jobInstance.dataTime = param.dataTime
        if (!param.executeParams.isNullOrBlank()) {
            jobInstance.executeParams = param.executeParams
        }
        jobInstanceRepository.save(jobInstance)
        val triggerTask = TriggerTask(
            jobInstance = jobInstance,
            nodeRegistryRepository = nodeRegistryRepository,
            jobInstanceRepository = jobInstanceRepository,
        )
        scheduler.addTask(jobInstance.jobName!!, LocalDateTime.now(), triggerTask)
        log.info("manually trigger success, jobId={}, jobInstanceId={}", param.jobId, jobInstance.id)
        return jobInstance.id!!
    }

    fun switch(param: JobInfoSwitchParam): Boolean {
        val jobInfo = findByIdOrThrow(param.jobId!!)
        val nextTriggerTime = jobInfo.takeIf { param.enabled!! }?.let {
            nextTriggerTime(scheduleType = it.scheduleType!!, scheduleConfig = it.scheduleConfig!!)
        }
        jobInfo.apply {
            this.enabled = param.enabled
            this.nextTriggerTime = nextTriggerTime
        }
        jobInfoRepository.saveAndFlush(jobInfo)
        return true
    }

    fun generateNextTriggerTime(
        scheduleType: ScheduleType,
        scheduleConfig: String,
        baseTime: LocalDateTime = LocalDateTime.now(),
        times: Int
    ): MutableList<LocalDateTime> {
        var currentBaseTime = baseTime
        val result = MutableList(times) { baseTime }
        repeat(times) { i ->
            val nextTime = nextTriggerTime(
                scheduleType=scheduleType,
                scheduleConfig = scheduleConfig,
                baseTime = currentBaseTime
            )
            result[i] = nextTime
            currentBaseTime = nextTime
        }
        return result
    }

    private fun nextTriggerTime(
        scheduleType: ScheduleType,
        scheduleConfig: String,
        baseTime: LocalDateTime = LocalDateTime.now()
    ): LocalDateTime {
        return when (scheduleType) {
            CRON -> {
                if (!CronExpression.isValidExpression(scheduleConfig)) {
                    throw BizException("非法表达式cron: $scheduleConfig")
                }
                CronExpression.parse(scheduleConfig).next(baseTime)!!
            }
            FIX_RATE -> baseTime
            FIX_DELAY -> baseTime
            ONE_TIME -> {
                try {
                    val nextTriggerTime = LocalDateTime.parse(scheduleConfig, DateTimeFormatter.ofPattern(STANDARD_DATE_FORMAT))
                    if (nextTriggerTime.isBefore(LocalDateTime.now().truncatedTo(ChronoUnit.DAYS))) {
                        throw BizException("调度时间不能早于今天")
                    }
                    return nextTriggerTime
                } catch (e: DateTimeParseException) {
                    throw BizException("非法时间格式: $scheduleConfig")
                }
            }
        }
    }

    private fun findByIdOrThrow(jobId: Long) = jobInfoRepository.findById(jobId)
        .orElseThrow { throw BizException("job is not exist, id=[$jobId]") }
}